﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using Antlr4.Runtime;
using FadeLang.Ast;
using FadeLang.CstVisitor;

namespace FadeLang.Runtime
{
    public class Context
    {
        private readonly Chunk mainChunk;

        public Context(string filePath) {
            var streamReader = new StreamReader(filePath, Encoding.UTF8);
            var inputStream = new AntlrInputStream(streamReader);
            var tokenSource = new FadeLangLexer(inputStream);
            var commonTokenStream = new CommonTokenStream(tokenSource);
            var langParser = new FadeLangParser(commonTokenStream);
            var context = new Context();
            mainChunk = langParser.chunk().Accept(new ChunkVisitor(context));
        }

        private Context() {
            NewScope();
        }

        private Scope CurrentScope { get; set; }

        public void Run() {
            Run(mainChunk);
        }

        private dynamic EvaluateExp(Exp exp) {
            if (exp.Value != null) {
                return exp.Value;
            }
            if (exp.Name != null) {
                return CurrentScope.GetValue(exp.Name);
            }
            if (exp.FunctionCall != null) {
                return Run(exp.FunctionCall);
            }
            switch (exp.Op) {
                case OpType.Null:
                    return null;

                case OpType.Add:
                    return EvaluateExp(exp.LeftExp) + EvaluateExp(exp.RightExp);

                case OpType.Minus:
                    if (exp.LeftExp != null && exp.RightExp == null) {
                        return -EvaluateExp(exp.LeftExp);
                    }
                    return EvaluateExp(exp.LeftExp) - EvaluateExp(exp.RightExp);

                case OpType.Mul:
                    return EvaluateExp(exp.LeftExp)*EvaluateExp(exp.RightExp);

                case OpType.Div:
                    return EvaluateExp(exp.LeftExp)/EvaluateExp(exp.RightExp);

                case OpType.Mod:
                    return EvaluateExp(exp.LeftExp)%EvaluateExp(exp.RightExp);

                case OpType.LessThan:
                    return EvaluateExp(exp.LeftExp) < EvaluateExp(exp.RightExp);

                case OpType.LessThanEquals:
                    return EvaluateExp(exp.LeftExp) <= EvaluateExp(exp.RightExp);

                case OpType.GreaterThan:
                    return EvaluateExp(exp.LeftExp) > EvaluateExp(exp.RightExp);

                case OpType.GreaterThanEquals:
                    return EvaluateExp(exp.LeftExp) >= EvaluateExp(exp.RightExp);

                case OpType.Equals:
                    return EvaluateExp(exp.LeftExp) == EvaluateExp(exp.RightExp);

                case OpType.And:
                    return EvaluateExp(exp.LeftExp) && EvaluateExp(exp.RightExp);

                case OpType.Or:
                    return EvaluateExp(exp.LeftExp) || EvaluateExp(exp.RightExp);

                case OpType.NotEquals:
                    return EvaluateExp(exp.LeftExp) != EvaluateExp(exp.RightExp);

                default:
                    throw new ArgumentOutOfRangeException();
            }
        }

        private string GetNamingDestroy(List<dynamic> args) {
            var sb = new StringBuilder();
            foreach (var t in args) {
                Type type = t.GetType();
                sb.Append(type.Name.Substring(0, 1));
            }
            return sb.ToString().ToLower();
        }

        private void NewScope() {
            CurrentScope = new Scope {ParentScope = CurrentScope};
        }

        private void NewScope(Scope scope) {
            scope.ParentScope = CurrentScope;
            CurrentScope = scope;
        }

        private void RemoveScope() {
            CurrentScope = CurrentScope.ParentScope;
        }

        private void Run(Chunk chunk) {
            foreach (var block in chunk.Blocks) {
                Run(block);
            }
        }

        private dynamic Run(Block block) {
            NewScope();
            dynamic returnValue = null;
            foreach (var statement in block.Statements) {
                if (statement is ReturnStatement) {
                    returnValue = Run((ReturnStatement) statement);
                    break;
                }
                Run(statement);
            }
            RemoveScope();
            return returnValue;
        }

        private void Run(DeclarationStatement statement) {
            CurrentScope.VariableObjects.Add(statement.VariableName, EvaluateExp(statement.VariableValue));
        }

        private void Run(FunctionDef function) {
            var functionNameWithArgumentCounts = $"{function.Name}`{function.Arguments.Count}";
            if (!CurrentScope.VariableObjects.ContainsKey(functionNameWithArgumentCounts)) {
                CurrentScope.VariableObjects.Add(functionNameWithArgumentCounts, function);
            }
            else {
                throw new Exception(
                    $"Function {functionNameWithArgumentCounts} is already defined in this scope!");
            }
        }

        private dynamic Run(ReturnStatement statement) {
            return EvaluateExp(statement.Value);
        }

        private dynamic Run(FunctionCall functionCall) {
            //eval value
            functionCall.ArgValueList.Clear();
            foreach (var exp in functionCall.ArgList) {
                functionCall.ArgValueList.Add(EvaluateExp(exp));
            }

            //destory function name
            functionCall.TypeNameList = GetNamingDestroy(functionCall.ArgValueList);

            var function = CurrentScope.GetValue(
                $"{functionCall.Name}`{functionCall.ArgValueList.Count}") as FunctionDef;

            dynamic returnValue = null;

            if (function != null) {
                var scope = new Scope();
                for (var i = 0; i < function.Arguments.Count; i++) {
                    scope.VariableObjects.Add(function.Arguments[i], functionCall.ArgValueList[i]);
                }

                NewScope(scope);
                foreach (var statement in function.FunctionBlock.Statements) {
                    returnValue = Run(statement);
                    if (returnValue != null) {
                        RemoveScope();
                        return returnValue;
                    }
                }
                RemoveScope();
                return returnValue;
            }

            if (InternalFunction.Instance.TryRun(functionCall, out returnValue)) {
                return returnValue;
            }
            throw new Exception($"Failed to call function {functionCall.Name}.");
        }

        private dynamic Run(Statement statement) {
            (statement != null).OrThrows("statement != null");
            if (statement is DeclarationStatement) {
                Run((DeclarationStatement) statement);
                return null;
            }
            if (statement is ReturnStatement) {
                Run((ReturnStatement) statement);
                return null;
            }
            if (statement is FunctionDef) {
                Run((FunctionDef) statement);
                return null;
            }
            if (statement is Assignment) {
                Run((Assignment) statement);
                return null;
            }
            if (statement is ImportStatement) {
                Run((ImportStatement) statement);
                return null;
            }
            if (statement is FunctionCall) {
                return Run((FunctionCall) statement);
            }
            if (statement is IfStatement) {
                return Run((IfStatement) statement);
            }
            if (statement is ForStatement) {
                return Run((ForStatement) statement);
            }
            throw new Exception("Internal Error:: Not defined statement.");
        }

        private void Run(ImportStatement statement) {
            var filePath = statement.ModulePath + ".flm";
            var streamReader = new StreamReader(filePath, Encoding.UTF8);
            var inputStream = new AntlrInputStream(streamReader);
            var tokenSource = new FadeLangLexer(inputStream);
            var commonTokenStream = new CommonTokenStream(tokenSource);
            var langParser = new FadeLangParser(commonTokenStream);
            var chunk = langParser.chunk().Accept(new ChunkVisitor(this));
            foreach (var block in chunk.Blocks) {
                foreach (var stat in block.Statements) {
                    Run(stat);
                }
            }
        }

        private dynamic Run(ForStatement statement) {
            dynamic returnValue = null;
            NewScope();
            Run(statement.DeclarationStatement);
            while (EvaluateExp(statement.Condition)) {
                returnValue = Run(statement.Block);
                if (returnValue != null) {
                    break;
                }
                Run(statement.Assignment);
            }
            RemoveScope();
            return returnValue;
        }

        private dynamic Run(IfStatement statement) {
            dynamic returnValue = null;
            var condition = EvaluateExp(statement.Condition);

            if (condition) {
                returnValue = Run(statement.TrueBlock);
            }
            else if (statement.ElseIfStatements != null) {
                foreach (var elseIfStatement in statement.ElseIfStatements) {
                    if (EvaluateExp(elseIfStatement.Condition)) {
                        returnValue = Run(elseIfStatement.TrueBlock);
                        return returnValue;
                    }
                }
            }
            else if (statement.ElseBlock != null) {
                returnValue = Run(statement.ElseBlock);
            }
            return returnValue;
        }

        private void Run(Assignment statement) {
            var current = CurrentScope;
            while (current != null) {
                if (current.VariableObjects.ContainsKey(statement.Name)) {
                    switch (statement.Op) {
                        case AssignmentOpType.Assignment:
                            current.VariableObjects[statement.Name] = EvaluateExp(statement.Value);
                            return;

                        case AssignmentOpType.AddAssignment:
                            current.VariableObjects[statement.Name] += EvaluateExp(statement.Value);
                            return;

                        case AssignmentOpType.SubAssignment:
                            current.VariableObjects[statement.Name] -= EvaluateExp(statement.Value);
                            return;

                        case AssignmentOpType.MulAssignment:
                            current.VariableObjects[statement.Name] *= EvaluateExp(statement.Value);
                            return;

                        case AssignmentOpType.DivAssignment:
                            current.VariableObjects[statement.Name] /= EvaluateExp(statement.Value);
                            return;

                        case AssignmentOpType.ModAssignment:
                            current.VariableObjects[statement.Name] %= EvaluateExp(statement.Value);
                            return;

                        default:
                            throw new ArgumentOutOfRangeException();
                    }
                }
                current = current.ParentScope;
            }
            throw new Exception($"{statement.Name} is not defined.");
        }
    }
}